using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Sapphire.Linq.Mappers
{
    public abstract class EnumerableMapper<T> : ObjectMapper<IEnumerable<T>>
    {
        protected override TTo Map<TTo>(IEnumerable<T> value, object[] parameters)
        {
            if (IsIEnumerableOfT<TTo>())
                return MapEnumerableLambda<TTo>(value, parameters);

            throw new NotSupportedException();
        }

        private readonly Dictionary<Type, Func<IEnumerable<T>, object[], object>> _mapEnumerableLambdas = new Dictionary<Type, Func<IEnumerable<T>, object[], object>>();

        private TTo MapEnumerableLambda<TTo>(IEnumerable<T> value, object[] parameters)
        {
            if (!_mapEnumerableLambdas.ContainsKey(typeof(TTo)))
            {
                var valueExpr = Expression.Parameter(typeof(IEnumerable<T>), "v");
                var parametersExpr = Expression.Parameter(typeof(object[]), "p");

                var call = Expression.Call(
                                Expression.Constant(this),
                                "MapEnumerable",
                                new[] { typeof(TTo).GetGenericArguments()[0] },
                                valueExpr, parametersExpr
                    );

                var lambda = Expression.Lambda<Func<IEnumerable<T>, object[], TTo>>(call, valueExpr, parametersExpr);
                var func = lambda.Compile();
                _mapEnumerableLambdas.Add(typeof(TTo), (v, ps) => func(v, ps));
            }

            return (TTo)_mapEnumerableLambdas[typeof(TTo)](value, parameters);
        }

        private static bool IsIEnumerableOfT<TP>()
        {
            if (typeof(IEnumerable).IsAssignableFrom(typeof(TP)))
                return true;

            return false;
        }

        public virtual IEnumerable<TTo> MapEnumerable<TTo>(IEnumerable<T> values, object[] parameters)
        {
            Func<T, TTo> map = i => Mapper.Instance.Map<TTo>(i, parameters);
            return values.Select(map);
        }
    }
}